import { Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import * as ObsClient from 'esdk-obs-nodejs';
import { v4 as uuidv4 } from 'uuid';

@Injectable()
export class ObsService {
  private readonly logger = new Logger(ObsService.name);
  private readonly obsEndpoint: string;
  private readonly obsBucket: string;
  private readonly obsAccessKey: string;
  private readonly obsSecretKey: string;
  private readonly defaultExpiration: number = 3600; // 1 hour in seconds
  private readonly maxFileSize: number = 10 * 1024 * 1024; // 10MB in bytes
  private obsClient: any;

  constructor(private readonly configService: ConfigService) {
    this.obsEndpoint = this.configService.get<string>('OBS_ENDPOINT');
    this.obsBucket = this.configService.get<string>('OBS_BUCKET');
    this.obsAccessKey = this.configService.get<string>('OBS_ACCESS_KEY');
    this.obsSecretKey = this.configService.get<string>('OBS_SECRET_KEY');

    // Validate configuration
    if (!this.obsEndpoint || !this.obsBucket || !this.obsAccessKey || !this.obsSecretKey) {
      this.logger.warn('OBS configuration is incomplete. Some features may not work correctly.');
    } else {
      // Initialize OBS client
      this.initObsClient();
    }
  }

  /**
   * Initialize the OBS client
   */
  private initObsClient(): void {
    try {
      this.obsClient = new ObsClient({
        access_key_id: this.obsAccessKey,
        secret_access_key: this.obsSecretKey,
        server: `https://${this.obsEndpoint}`,
        max_retry_count: 3,
        timeout: 60,
      });
      this.logger.log('OBS client initialized successfully');
    } catch (error) {
      this.logger.error(`Failed to initialize OBS client: ${error.message}`, error.stack);
    }
  }

  /**
   * Normalize the resource path to ensure it doesn't start with a leading slash
   * @param resourcePath The resource path to normalize
   * @returns Normalized resource path
   */
  private normalizeResourcePath(resourcePath: string): string {
    // Remove leading slash if present
    return resourcePath.startsWith('/') ? resourcePath.substring(1) : resourcePath;
  }

  /**
   * Generate a pre-signed URL for uploading a file to OBS
   * @param resourceName Resource name to use in the object key
   * @param expiresIn Expiration time in seconds (default: 1 hour)
   * @returns Pre-signed URL, expiration time, and headers
   */
  async generateUploadUrl(
    resourceName: string,
    expiresIn: number = this.defaultExpiration,
  ): Promise<{
    url: string;
    expiresIn: number;
    headers: Record<string, string>;
  }> {
    try {
      // Extract directory path and filename
      const lastSlashIndex = resourceName.lastIndexOf('/');
      const hasPath = lastSlashIndex !== -1;

      let dirPath = '';
      let fileName = resourceName;

      if (hasPath) {
        dirPath = resourceName.substring(0, lastSlashIndex + 1);
        fileName = resourceName.substring(lastSlashIndex + 1);
      }

      // Generate unique identifiers for the path
      const uuid = uuidv4();
      const timestamp = Date.now();
      const uniqueDir = `${timestamp}-${uuid}`;

      let objectKey;
      if (resourceName.startsWith('/')) {
        // Preserve the exact path but add uniqueDir before the filename
        const normalizedPath = this.normalizeResourcePath(dirPath);
        objectKey = `${normalizedPath}${uniqueDir}/${fileName}`;
      } else if (hasPath) {
        // Has a path but doesn't start with '/', add to uploads/ and preserve structure
        objectKey = `uploads/${dirPath}${uniqueDir}/${fileName}`;
      } else {
        // No path, just a filename
        objectKey = `uploads/${uniqueDir}/${fileName}`;
      }

      // Define all headers that will be used for signing
      const headers = {
        'x-obs-acl': 'private',
      };

      // Generate a pre-signed URL for PUT operation
      const result = this.obsClient.createSignedUrlSync({
        Method: 'PUT',
        Bucket: this.obsBucket,
        Key: objectKey,
        Expires: expiresIn,
        Headers: {
          ...headers,
          'Content-Length-Range': `0,${this.maxFileSize}`, // Limit file size to 10MB
        },
      });

      if (!result.SignedUrl) {
        throw new Error('Failed to generate pre-signed upload URL');
      }

      this.logger.debug(`Generated pre-signed upload URL for object key: ${objectKey}`);

      // Log the StringToSign for debugging
      if (result.StringToSign) {
        this.logger.debug(`StringToSign: ${result.StringToSign}`);
      }

      return {
        url: result.SignedUrl,
        expiresIn,
        headers,
      };
    } catch (error) {
      this.logger.error(`Failed to generate pre-signed upload URL: ${error.message}`, error.stack);
      throw new Error('Failed to generate upload URL');
    }
  }

  /**
   * Generate multiple pre-signed URLs for uploading files to OBS in batch
   * @param resourceNames Array of resource names to use in the object keys
   * @param expiresIn Expiration time in seconds (default: 1 hour)
   * @returns Array of pre-signed URLs, expiration times, and headers
   */
  async generateBatchUploadUrls(
    resourceNames: string[],
    expiresIn: number = this.defaultExpiration,
  ): Promise<
    {
      url: string;
      expiresIn: number;
      resourceName: string;
      headers: Record<string, string>;
    }[]
  > {
    try {
      this.logger.debug(`Generating batch upload URLs for ${resourceNames.length} resources`);

      // Generate pre-signed URLs for each resource name
      const uploadUrls = await Promise.all(
        resourceNames.map(async (resourceName) => {
          const {
            url,
            expiresIn: expiration,
            headers,
          } = await this.generateUploadUrl(resourceName, expiresIn);

          return {
            url,
            expiresIn: expiration,
            resourceName,
            headers,
          };
        }),
      );

      return uploadUrls;
    } catch (error) {
      this.logger.error(`Failed to generate batch upload URLs: ${error.message}`, error.stack);
      throw new Error('Failed to generate batch upload URLs');
    }
  }

  /**
   * Generate a pre-signed URL for downloading a file from OBS
   * @param objectKey The object key in OBS
   * @param expiresIn Expiration time in seconds (default: 1 hour)
   * @returns Pre-signed URL and expiration time
   */
  async generateDownloadUrl(
    objectKey: string,
    expiresIn: number = this.defaultExpiration,
  ): Promise<{ url: string; expiresIn: number }> {
    try {
      // Normalize the object key to ensure it doesn't have a leading slash
      const normalizedKey = this.normalizeResourcePath(objectKey);

      // Generate a pre-signed URL for GET operation
      const result = this.obsClient.createSignedUrlSync({
        Method: 'GET',
        Bucket: this.obsBucket,
        Key: normalizedKey,
        Expires: expiresIn,
      });

      if (!result.SignedUrl) {
        throw new Error('Failed to generate pre-signed download URL');
      }

      this.logger.debug(`Generated pre-signed download URL for object key: ${normalizedKey}`);

      return {
        url: result.SignedUrl,
        expiresIn,
      };
    } catch (error) {
      this.logger.error(
        `Failed to generate pre-signed download URL: ${error.message}`,
        error.stack,
      );
      throw new Error('Failed to generate download URL');
    }
  }

  /**
   * Extract object key from a file URL
   * @param fileUrl The file URL
   * @returns The object key
   */
  extractObjectKeyFromUrl(fileUrl: string): string {
    try {
      const url = new URL(fileUrl);
      // Remove the leading slash if present
      const pathname = url.pathname.startsWith('/') ? url.pathname.substring(1) : url.pathname;
      return pathname;
    } catch (error) {
      this.logger.error(`Failed to extract object key from URL: ${error.message}`, error.stack);
      throw new Error('Invalid file URL');
    }
  }

  /**
   * Validate if a file URL belongs to our OBS bucket
   * @param fileUrl The file URL to validate
   * @returns True if the URL is valid, false otherwise
   */
  validateFileUrl(fileUrl: string): boolean {
    try {
      const url = new URL(fileUrl);
      // Check if the URL hostname contains our bucket and endpoint
      return url.hostname.includes(this.obsBucket) && url.hostname.includes(this.obsEndpoint);
    } catch (error) {
      this.logger.error(`Invalid file URL: ${fileUrl}`, error.stack);
      return false;
    }
  }

  /**
   * Delete an object from OBS
   * @param objectKey The object key to delete
   * @returns Promise resolving to true if deletion was successful
   */
  async deleteObject(objectKey: string): Promise<boolean> {
    try {
      // Normalize the object key to ensure it doesn't have a leading slash
      const normalizedKey = this.normalizeResourcePath(objectKey);

      const result = await this.obsClient.deleteObject({
        Bucket: this.obsBucket,
        Key: normalizedKey,
      });

      if (result.CommonMsg.Status < 300) {
        this.logger.debug(`Successfully deleted object with key: ${normalizedKey}`);
        return true;
      } else {
        this.logger.error(`Failed to delete object: ${result.CommonMsg.Message}`);
        return false;
      }
    } catch (error) {
      this.logger.error(`Error deleting object: ${error.message}`, error.stack);
      return false;
    }
  }
}
